package org.anderground.app.storage.xml;

import static org.anderground.app.storage.xml.XmlConfigConstants.ATTR_CODE;
import static org.anderground.app.storage.xml.XmlConfigConstants.ATTR_COUNT;
import static org.anderground.app.storage.xml.XmlConfigConstants.ATTR_COUNTRY;
import static org.anderground.app.storage.xml.XmlConfigConstants.ATTR_LATITUDE;
import static org.anderground.app.storage.xml.XmlConfigConstants.ATTR_LONGITUDE;
import static org.anderground.app.storage.xml.XmlConfigConstants.ATTR_VERSION;
import static org.anderground.app.storage.xml.XmlConfigConstants.TAG_ALIAS;
import static org.anderground.app.storage.xml.XmlConfigConstants.TAG_CITIES;
import static org.anderground.app.storage.xml.XmlConfigConstants.TAG_CITY;
import static org.anderground.app.storage.xml.XmlConfigConstants.TAG_COUNTRIES;
import static org.anderground.app.storage.xml.XmlConfigConstants.TAG_COUNTRY;
import static org.anderground.app.storage.xml.XmlConfigConstants.TAG_LOCATION;
import static org.anderground.app.storage.xml.XmlConfigConstants.TAG_NAME;
import static org.anderground.app.storage.xml.XmlConfigConstants.TAG_ROOT;
import static org.anderground.app.storage.xml.XmlConfigConstants.TAG_URL;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.anderground.app.storage.ConfigData;
import org.anderground.app.storage.OnProgressCallback;
import org.anderground.core.Logger;
import org.anderground.core.model.IdBindingModelException;
import org.anderground.core.storage.StorageException;
import org.anderground.modules.storage.xml.HierarchyHandler;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;

/**
 * XmlReader implementation that uses SAX to read the file.
 * 
 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
 */
public class SaxConfigReader extends XmlConfigReader {

	/**
	 * @param file
	 * @param callback
	 */
	public SaxConfigReader(File file, OnProgressCallback callback) {
		super(file, callback);
	}

	/**
	 * @param inputStream
	 * @param closeAfterRead
	 * @param callback
	 */
	public SaxConfigReader(InputStream inputStream, boolean closeAfterRead,
			OnProgressCallback callback) {
		super(inputStream, closeAfterRead, callback);
	}

	@Override
	protected ConfigData readFromInput(InputStream inputStream)
			throws IOException {

		ConfigData.Builder builder = new ConfigData.Builder();

		try {
			SAXParserFactory parserFactory = SAXParserFactory.newInstance();
			parserFactory.setFeature("http://xml.org/sax/features/namespaces",
					false);
			parserFactory.setFeature(
					"http://xml.org/sax/features/namespace-prefixes", true);
			SAXParser parser = parserFactory.newSAXParser();

			XMLReader reader = parser.getXMLReader();

			reader.setFeature("http://xml.org/sax/features/namespaces", false);
			reader.setFeature("http://xml.org/sax/features/namespace-prefixes",
					true);

			reader.setContentHandler(new RootHierarchyHandler(reader, builder,
					getOnProgressCallback()));

			InputSource inputSource = new InputSource(inputStream);
			reader.parse(inputSource);

		} catch (SAXException e) {
			throw new StorageException("SAX parsing failed", e);
		} catch (ParserConfigurationException e) {
			throw new StorageException(e);
		}

		try {
			return builder.build();
		} catch (IdBindingModelException e) {
			throw new StorageException("config building failed", e);
		}

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private static interface HandlerCommons {

		public abstract String getVersion();

		public abstract XMLReader getReader();

		public abstract ConfigData.Builder getBuilder();

		public abstract int getTotalCitiesCount();

		public abstract OnProgressCallback getOnProgressCallback();

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private static abstract class AbstractHierarchyHandler extends
			HierarchyHandler implements HandlerCommons {

		@Override
		protected final void enter(HierarchyHandler childHandler) {
			super.enter(childHandler);
			if (childHandler instanceof HandlerCommons) {
				getReader().setContentHandler(childHandler);
			}
		}

		@Override
		protected final HierarchyHandler leave() {
			HierarchyHandler handler = super.leave();
			getReader().setContentHandler(handler);
			return handler;
		}

		@Override
		protected void setRoot(HierarchyHandler root) {
			if (root instanceof AbstractHierarchyHandler) {
				setRoot((AbstractHierarchyHandler) root);
			}
		}

		protected abstract void setRoot(AbstractHierarchyHandler root);

		protected abstract AbstractHierarchyHandler getRoot();

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private static abstract class ChildHierarchyHandler extends
			AbstractHierarchyHandler {

		private AbstractHierarchyHandler root;

		public ChildHierarchyHandler() {
		}

		@Override
		public final String getVersion() {
			return root.getVersion();
		}

		@Override
		public final XMLReader getReader() {
			return root.getReader();
		}

		@Override
		public final ConfigData.Builder getBuilder() {
			return root.getBuilder();
		}

		@Override
		public final int getTotalCitiesCount() {
			return root.getTotalCitiesCount();
		}

		@Override
		public final OnProgressCallback getOnProgressCallback() {
			return root.getOnProgressCallback();
		}

		@Override
		protected AbstractHierarchyHandler getRoot() {
			return root;
		}

		@Override
		protected void setRoot(AbstractHierarchyHandler root) {
			this.root = root;
		}

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private final static class RootHierarchyHandler extends
			AbstractHierarchyHandler {

		private XMLReader reader;
		private ConfigData.Builder builder;
		private OnProgressCallback callback;

		private String version = null;
		/**
		 * used when invoking
		 * {@link OnProgressCallback#onProgress(int, int,String)}
		 */
		private int citiesCount = 0;

		public RootHierarchyHandler(XMLReader reader,
				ConfigData.Builder builder, OnProgressCallback callback) {
			this.reader = reader;
			this.builder = builder;
			this.callback = callback;
		}

		@Override
		protected HierarchyHandler onStartChild(String name,
				Attributes attributes) {
			if (name == (TAG_ROOT)) {
				this.version = attributes.getValue(ATTR_VERSION).intern();
				try {
					this.citiesCount = Integer.parseInt(attributes
							.getValue(ATTR_COUNT));
				} catch (NumberFormatException e) {
					// noop
				}
				Logger.d("xml file version retrieved: " + getVersion());
				return new ConfigHandler();

			}
			return this;
		}

		@Override
		public final String getVersion() {
			return version;
		}

		@Override
		public final XMLReader getReader() {
			return reader;
		}

		@Override
		public final ConfigData.Builder getBuilder() {
			return builder;
		}

		@Override
		public final int getTotalCitiesCount() {
			return citiesCount;
		}

		@Override
		public final OnProgressCallback getOnProgressCallback() {
			return callback;
		}

		@Override
		protected final AbstractHierarchyHandler getRoot() {
			return this;
		}

		@Override
		protected final void setRoot(AbstractHierarchyHandler root) {
			// noop
		}

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private static class ConfigHandler extends ChildHierarchyHandler {

		@Override
		protected HierarchyHandler onStartChild(String name,
				Attributes attributes) {
			if (name == (TAG_CITIES)) {
				return new CitiesHandler();

			} else if (name == (TAG_COUNTRIES)) {
				OnProgressCallback c = getOnProgressCallback();
				if (c != null) {
					c.onProgress("(countries)");
				}
				return new CountriesHandler();

			}
			return this;
		}

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private static class CitiesHandler extends ChildHierarchyHandler {

		private int readCities = 0;

		@Override
		protected HierarchyHandler onStartChild(String name, Attributes attrs) {
			if (name == (TAG_CITY)) {
				String countryCode = attrs.getValue(ATTR_COUNTRY);
				if (countryCode == null || countryCode.length() == 0) {
					return new SilentHandler();
				}
				getBuilder().startCity(countryCode);

			} else if (name == (TAG_LOCATION)) {
				try {
					int latitude = Integer.parseInt(attrs
							.getValue(ATTR_LATITUDE));
					int longitude = Integer.parseInt(attrs
							.getValue(ATTR_LONGITUDE));
					getBuilder().setCityLocation(latitude, longitude);
				} catch (NumberFormatException e) {
					// noop
				}

			}
			return this;
		}

		@Override
		protected void onNonWhiteString(String name, String string)
				throws SAXException {
			if (name == (TAG_NAME)) {
				OnProgressCallback c = getOnProgressCallback();
				if (c != null) {
					c.onProgress(++readCities, getTotalCitiesCount(), string);
				}
				getBuilder().setCityName(string);

			} else if (name == (TAG_ALIAS)) {
				getBuilder().addCityAlias(string);

			} else if (name == (TAG_URL)) {
				try {
					getBuilder().setUrl(new URL(string));
				} catch (MalformedURLException e) {
					Logger.w("MalformedURLException when reading ConfigCity");
				}

			}

		}

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private static class CountriesHandler extends ChildHierarchyHandler {

		@Override
		protected HierarchyHandler onStartChild(String name, Attributes attrs) {
			if (name == (TAG_COUNTRY)) {
				String countryCode = attrs.getValue(ATTR_CODE);
				if (countryCode == null || countryCode.length() == 0) {
					return new SilentHandler();
				}
				getBuilder().startCountry(countryCode);

			}
			return this;
		}

		@Override
		protected void onNonWhiteString(String name, String string)
				throws SAXException {
			if (name == (TAG_COUNTRY)) {
				getBuilder().setCountryName(string);

			}

		}

	}

	/**
	 * @author Eldar Abusalimov (eldar.abusalimov@gmail.com)
	 */
	private static class SilentHandler extends ChildHierarchyHandler {
	}

}